﻿using System;
using System.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.Framework;
using EPiServer.Security;
using EPiServer.Web;
using TemplateFoundation.Configuration;
using TemplateFoundation.Core;
using TemplateFoundation.Exceptions;
using TemplateFoundation.PageTypes;
using TemplateFoundation.Tags;

namespace TemplateFoundation.EventHandlers
{
    /// <summary>
    /// Used to handle publish events for taggable pages
    /// </summary>
    [InitializableModule]
    [ModuleDependency(typeof(PageTypeBuilder.Initializer))]
    public class PageTags : IInitializableModule
    {
        private bool _eventHandlersAttached = false;

        #region IInitializableModule Members

        /// <summary>
        /// Initializes the specified context.
        /// </summary>
        /// <param name="context">The context.</param>
        public void Initialize(EPiServer.Framework.Initialization.InitializationEngine context)
        {
            if (_eventHandlersAttached)
            {
                return;
            }

            // Verify a tags container exists and that a tag page type has been specified before continuing
            try
            {
                if (PageReference.IsNullOrEmpty(Settings.Instance.TagsContainer) || !Settings.Instance.TagPageTypeId.HasValue)
                {
                    // We haven't added any event handlers, but none are required since tag configuration is completed
                    _eventHandlersAttached = true;

                    return;
                }

                DataFactory.Instance.PublishingPage += PublishingPage;
                DataFactory.Instance.MovingPage += MovingPage;
                DataFactory.Instance.DeletingPage += DeletingPage;

                _eventHandlersAttached = true;
            }
            catch (TemplateFoundationException)
            {
                // TODO Log this error, but don't let the exception bubble up (we may be in a state where the tags container simply hasn't been set yet)

                return;
            }
        }

        /// <summary>
        /// Not implemented
        /// </summary>
        /// <param name="parameters">The parameters.</param>
        public void Preload(string[] parameters)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Uninitializes the specified context and unwires event handlers
        /// </summary>
        /// <param name="context">The context.</param>
        public void Uninitialize(EPiServer.Framework.Initialization.InitializationEngine context)
        {
            DataFactory.Instance.PublishingPage -= PublishingPage;
            DataFactory.Instance.MovingPage -= MovingPage;
            DataFactory.Instance.DeletingPage -= DeletingPage;
        }

        #endregion

        void PublishingPage(object sender, PageEventArgs e)
        {
            if (e.Page is PageTypeBase)
            {
                RefreshTags((PageTypeBase)e.Page);
            }
        }

        void DeletingPage(object sender, PageEventArgs e)
        {
            var page = e.Page as PageTypeBase;

            if (page==null)
            {
                return;
            }

            foreach (var tag in page.Tags)
            {
                RegisterRemovedTag(page, tag);
            }
        }

        void MovingPage(object sender, PageEventArgs e)
        {
            var page = e.Page as PageTypeBase;

            if (page==null)
            {
                return;
            }

            if (Settings.Instance.TagPageTypeId.HasValue)
            {
                if (page.PageTypeID==Settings.Instance.TagPageTypeId.Value) // If trying to move a tag page
                {
                    throw new TemplateFoundationException("Tag pages cannot be moved or deleted when one or more pages use the tag");
                }
            }

            if (e.TargetLink==PageReference.WasteBasket) // Page is 'deleted' (moved to the waste basket)
            {
                foreach (var tag in page.Tags)
                {
                    RegisterRemovedTag(page, tag);
                }
            }

            if (page.ParentLink==PageReference.WasteBasket) // Page is 'restored' (moved back from the waste basket)
            {
                foreach (var tag in page.Tags)
                {
                    UpdateTagPage(page, tag);
                }
            }
        }

        /// <summary>
        /// Updates tag pages for the specified page
        /// </summary>
        /// <param name="page"></param>
        protected virtual void RefreshTags(PageTypeBase page)
        {
            // We exclude RSS pages because they only use the Tags property for filtering, so RSS pages shouldn't be included in tag statistics
            if (page.Tags==null || page is RssPageBase)
            {
                return;
            }

            // Examine tags from last published page version to see if any tags have been removed
            var allVersions = (from pv in PageVersion.List(page.PageLink, page.LanguageBranch)
                               where pv.Status==VersionStatus.Published || pv.Status==VersionStatus.PreviouslyPublished
                               select pv).ToArray();

            if (allVersions.Length>0)
            {
                var tagsBefore = ((PageTypeBase)DataFactory.Instance.GetPage(allVersions.Last().ID)).Tags;

                // Find tags that have been removed since page was last published
                foreach (var tag in tagsBefore)
                {
                    if (!page.Tags.Contains(tag))
                    {
                        RegisterRemovedTag(page, tag);
                    }
                }

                // Find new tags that have been added since the page was last published
                foreach (var tag in page.Tags)
                {
                    if (!tagsBefore.Contains(tag))
                    {
                        UpdateTagPage(page, tag);
                    }
                }
            }
            else // Page has not been published before
            {
                foreach (var tag in page.Tags)
                {
                    UpdateTagPage(page, tag);
                }
            }
        }

        /// <summary>
        /// Updates the page count of a tag page after a tag has been removed
        /// </summary>
        /// <param name="page"></param>
        /// <param name="tag"></param>
        protected virtual void RegisterRemovedTag(PageTypeBase page, Tag tag)
        {
            // Create or modify tag page for the tag
            var link = tag.GetPageLink();

            if (!PageReference.IsNullOrEmpty(link))
            {
                var tagPage = (ArticleTagListingPageBase)link.GetPage();

                if (!tagPage.IsMasterLanguageBranch) // PageCount is not language-specific, we need to update the master language branch
                {
                    tagPage = (ArticleTagListingPageBase)link.GetPage(new LanguageSelector(tagPage.MasterLanguageBranch));
                }

                if (tagPage.PageCount>1)
                {
                    var editable = (ArticleTagListingPageBase)tagPage.CreateWritableClone();
                    editable.PageCount--;
                    DataFactory.Instance.Save(editable, SaveAction.Publish | SaveAction.ForceCurrentVersion,
                                              AccessLevel.NoAccess);
                }
                else
                {
                    // Delete the tag page since it's no longer needed
                    DataFactory.Instance.Delete(link, true, AccessLevel.NoAccess);
                }
            }
        }

        /// <summary>
        /// Creates or updates the tag page for the specified page and tag
        /// </summary>
        /// <param name="tag"></param>
        /// <param name="page"></param>
        protected virtual void UpdateTagPage(PageTypeBase page, Tag tag)
        {
            // Create or modify tag page for the tag
            var link = tag.GetPageLink();

            if (PageReference.IsNullOrEmpty(link))
            {
                var newTagPage = (ArticleTagListingPageBase)DataFactory.Instance.GetDefaultPageData(
                    Settings.Instance.TagsContainer,
                    Settings.Instance.TagPageTypeId.Value,
                    new LanguageSelector(page.LanguageBranch));

                newTagPage.PageCount = 1;
                newTagPage.PageName = tag.Name;
                newTagPage.URLSegment=UrlSegment.CreateUrlSegment(newTagPage);

                DataFactory.Instance.Save(newTagPage, SaveAction.Publish, AccessLevel.NoAccess);
            }
            else // Tag page already exists, increment page count
            {
                var tagPage = link.GetPage(new LanguageSelector(page.LanguageBranch));

                if (tagPage==null) // Tag page exists, but not for the current language branch
                {
                    tagPage = link.GetPage(); // Get the other language branch to copy properties

                    var newLanguageBranch = DataFactory.Instance.CreateLanguageBranch(tagPage.PageLink,
                                                                                      new LanguageSelector(
                                                                                          page.LanguageBranch));

                    newLanguageBranch.PageName = tagPage.PageName;
                    newLanguageBranch.URLSegment = tagPage.URLSegment;
                    newLanguageBranch.StartPublish = tagPage.StartPublish;

                    DataFactory.Instance.Save(newLanguageBranch, SaveAction.Publish, AccessLevel.NoAccess);
                }

                // Update PageCount for the master language branch
                if (!tagPage.IsMasterLanguageBranch) // PageCount is not language-specific, we need to update the master language branch
                {
                    tagPage = link.GetPage(new LanguageSelector(tagPage.MasterLanguageBranch));
                }

                var copy = (ArticleTagListingPageBase)tagPage.CreateWritableClone();

                try
                {
                    copy.PageCount++;
                }
                catch
                {
                    copy.PageCount = 1;
                }

                DataFactory.Instance.Save(copy, SaveAction.ForceCurrentVersion | SaveAction.Publish,
                                          AccessLevel.NoAccess);
            }
        }
    }
}
